//-----------------------------------------------------------------------------
// Filename: SecureXmlController.cs
//
// Description: 
// Provides either a file or database authenticatation mechanism. Authentication
// is simple username and password combinations which should be performed over
// SSL if there is anything significant being protected.
//
// History:
// 24 Jan 2004	Aaron Clauson	Created.
// 04 Sep 2004	Aaron Clauson	Minor changes.
// 11 Dec 2004	Aaron Clauson	Added lastpage functionality.
//
// License: 
// Public Domain.
//-----------------------------------------------------------------------------

using System;
using System.Collections;
using System.IO;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml;
using MaverickLite.Config;
using MaverickLite.Controller;
using BlueFace.Sys;
using log4net;

namespace MaverickLite.Security
{
	/// <summary>
	/// Controller that provides common services to derived controllers.
	/// Services this controller provides,
	/// 1. Attempts to extract a list of paths from the controller initialistaion parameters,
	/// 2. Attempts to extract a controller specific security directory path from the initialisation parameters.
	/// 
	/// Pointers:
	/// 1. Set a <param key="authentication" value="true|transparent"/> key for the controller.
	/// 2. To login the user agent HTTP request needs to send "authaction=login" and use 
	///    "username" and "password" fields to pass up credentials.
	/// 3. If successfully authenticated a request will be made to the user agent to set a cookie 
	///    named "authid" which can then be used for authentication in subsequent requests.
	/// 4. To logout of a session the user agent needs to send a "authaction=logout" field in the HTTP request.
	/// 5. The method of authentication can be relatively easily changed. By default an XML file holding valid 
	///    credentials is used. Note also that an authenticator is created for each different controller
	///    allowing each command to potentially use a different mechanism/directory if you wanted to go that granular.
	/// 6. If authenticated an "<identity>username</identity>" will be added to the Model and the m_authenticated 
	///    property will be set to true.
	/// 7. If <param key="authentication" value="true" /> and the request is authenticated then the view will be
	///    set to authenticated, if not it will be set to unauthenticated.
	/// 8. If <param key="authentication" value="transparent" /> the request will be processed exactly the same
	///    but the view will not be set to authenticated or unauthenticated. This is for pages that can make use
	///    of the fact that the user has been authenticated but that don't require it.
	/// </summary>
	public class SecureXmlController : XmlController
	{
		//private const string DIRECTORY_PATH_KEY = "securityDirectoryPath";	// Application configuration parameter key for the path to the security directory.
		public const string AUTHENTICATION_KEY = "authentication";
		public const string AUTHENTICATIONSTORE_ID_KEY = "authstoreid";
		//public const string AUTHENTICATION_DATABASE_KEY = "authenticationDbConnectionString";
		//public const string AUTHENTICATION_FILEPATH_KEY = "authenticationDirectoryPath";
		//public const string AUTHENTICATION_TIMEOUT_KEY = "authenticationTimeout";

		// HTTP request keys dealing with authentication.
		public const string AUTHENTICATION_STATE_KEY = "authid";			// The name of the state field used to contain authentication state information.
		public const string AUTHENTICATION_USERNAME_KEY = "username";		// The name field for usernames in HTTP requests.
		public const string AUTHENTICATION_PASSWORD_KEY = "password";		// The name field for passwords in HTTP requests.
		public const string AUTHENTICATION_ACTION_KEY = "authaction";		// The name of the field used in HTTP requests to request an authentication action.

		public const string REQUEST_LASTPAGE_KEY = "lastpage";				// The name of the HTTP request parameter indicating where an unauthencated message was recieved.

		protected AuthenticatorProduct m_authenticator;		// Provides authentication services.

		// Request parameter values.
		protected bool m_authenticated;						// Indicates whether the request has been authenticated or not.
		protected Credentials m_authenticatedCredentials;	// Holds credentials if the request was successfully authenticated.
		protected string m_authenticationAction;			// The authentication action required for the controller.
		protected string m_authStoreId;						// The authentication id for the store required for this operation.
		protected string m_lastPage;						// If available holds the value of the page that redirected an authentication request.
		//protected string m_timeoutMinutes;				// If available in the controller parameters this value will be used to set how long authnetication sessions stay valid for.
		protected string m_authPageStoreSettings;				// The authentication store settings that were used to authenticate this request.

		//private string m_authDirectoryPath = MavAppState.AuthenticationDirectoryPath;
		//private string m_appAuthenticatorDBString = MavAppState.AuthenticatorDBConnString;
		//private StorageTypes m_authenticatorStorageType = MavAppState.AuthenticatorDBType;
		//private string m_authenticatorTimeout = MavAppState.AuthenticationTimeoutMinutes;

		private static Hashtable m_authStoreSettings = MavAppState.AuthenticationStores;
		private static Hashtable m_authStoreTypes = MavAppState.AuthenticationStoreTypes;
		private static string m_defaultAuthId = MavAppState.DefaultAuthenticationId;

		protected bool m_addSystemSecurityModel = true;				// Determines whether to add the system security XML to the page model.

		/// <summary> 
		/// </summary>
		/// <param name="initParams">The xml element containing the controller's intial parameters.</param>
		public SecureXmlController(XmlElement initParams) : base(initParams)
		{
			//logger.Debug("SecureXmlController object created.");
		}

		/// <summary>
		/// Initialises the fields required for a page request. This method is called at the start of the
		/// controller stage for each incoming request.
		/// </summary>
		/// <param name="context">The Http context of the incoming request.</param>
		protected override void InitialiseRequest(HttpContext context)
		{
			//logger.Debug("SecureXmlController InitialiseRequest.");
			AuthenticatorFactory factory = new AuthenticatorFactory();

			// Authentication field initialisations.
			m_authenticated = false;
			m_authenticatedCredentials = null;

			// Get controller parameters.
			m_authenticationAction = (string)this.SimpleParameters[AUTHENTICATION_KEY];
			m_authStoreId = (string)this.SimpleParameters[AUTHENTICATIONSTORE_ID_KEY]; 
			m_lastPage = this.MavRequestState.GetRequestValue(REQUEST_LASTPAGE_KEY);

			// Workflow field initialisations
			m_view = "success";

			// Check if authentication is required.
			if(m_authenticationAction != null && Regex.Match(m_authenticationAction, "true|transparent", RegexOptions.IgnoreCase).Success)
			{
				// Initialise authenticator.
				if(m_authenticator == null)
				{
					if(m_authStoreId != null && m_authStoreId.Trim().Length > 0)
					{
						m_authPageStoreSettings = (string)m_authStoreSettings[m_authStoreId];
						
						m_authenticator = factory.CreateAuthenticator(
							(AuthenticatorTypesEnum)m_authStoreTypes[m_authStoreId], 
							m_authPageStoreSettings); 
					}
					else if(m_defaultAuthId != null && m_defaultAuthId.Trim().Length > 0)
					{
						m_authPageStoreSettings = (string)m_authStoreSettings[m_defaultAuthId];
						
						m_authenticator = factory.CreateAuthenticator(
							(AuthenticatorTypesEnum)m_authStoreTypes[m_defaultAuthId], 
							m_authPageStoreSettings); 
					}
					else
					{
						throw new ApplicationException("An authenticator cannot be created with the supplied authid " + m_authStoreId + ".");
					}
				}
								
				// Perform the authentication.
				m_authenticatedCredentials = Authenticate(m_authenticator, MavRequestState, MavSessionState, m_authenticationAction);

				// If credentials are present then authentication was successful.
				if(m_authenticatedCredentials != null)
				{
					m_authenticated = true;
				}
				
				// If this request marked authentication as mandatory set the view.
				if(m_authenticationAction != null && Regex.Match(m_authenticationAction, "true", RegexOptions.IgnoreCase).Success)
				{
					if(m_authenticated)
					{
						m_view = "authenticated";
					}
					else
					{
						m_view = "unauthenticated";
					}
				}
			}

			logger.Debug("SecureXMLController Initialise Finished: " + DateTime.Now.ToString("dd MMM yyyy HH:mm:ss:fff"));
		}

		/// <summary>
		/// If the request came from a page that wasn't authenticated but wishes to return the user to itself 
		/// once they have been authenticated. This is done by setting the last page HTTP request field.
		/// </summary>
		/// <returns></returns>
		public override string Perform()
		{
			if(m_view == "authenticated" && m_lastPage != null && m_lastPage.Trim().Length != 0)
			{
				return m_lastPage;
			}
			else
			{
				return null;
			}
		}

		/// <summary>
		/// Template method that should be used to construct the page specific model for the request.
		/// </summary>
		public override void BuildPageModel()
		{
			// Do nothing.
			logger.Debug("SecureXMLController BuildPageModel.");
		}

		public void AddIdentityMessage(AuthenticatorProduct authenticator, string sessionId)
		{
			if(m_addSystemSecurityModel)
			{
				m_pageModel.AddToModel("<page><identity>" + authenticator.GetUsernameForSessionId(sessionId) + "</identity></page>", false);
				m_pageModel.AddToModel("<page><authid>" + sessionId + "</authid></page>", false);
			}
		}

		public void AddEditRequest(string actionUrl)
		{
			m_pageModel.AddToModel("<page><editAction action='" + actionUrl + "'>edit</editAction></page>", true);
		}
		
		public void AddAuthenticationRequest()
		{
			m_pageModel.AddToModel("<page><authenticate>basic</authenticate></page>", true);
		}

		public Credentials Authenticate(AuthenticatorProduct authenticator, RequestState requestState, SessionState sessionState, string authenticationAction)
		{		
			string validSessionId = null;	// If authenticated the a valid session id will be generated.
			
			// No point going any further if the object doing the authentication is not available.
			if(authenticator != null)
			{
				string action = MavRequestState.GetRequestValue(AUTHENTICATION_ACTION_KEY);		// Check what the requested authentication action was.	

				string authId = MavRequestState.GetRequestValue(AUTHENTICATION_STATE_KEY);
				string username = MavRequestState.GetRequestValue(AUTHENTICATION_USERNAME_KEY);	// Authentication username field.
				string password = MavRequestState.GetRequestValue(AUTHENTICATION_PASSWORD_KEY);	// Authentication password field.
						

				// If a username has been supplied assume a new login request; ignore any current session.
				if((username == null || username.Trim().Length == 0) && (authId == null || authId.Trim().Length == 0))
				{
					authId = MavRequestState.GetCookieValue(AUTHENTICATION_STATE_KEY);			// Authentication session state field identifying previous authentication.
				}

				// Create a set of credentials from the reqest information.
				Credentials requestCredentials = new Credentials(username, password, authId);

				if(action != "logout")
				{
					validSessionId = authenticator.AuthenticateWebRequest(requestCredentials, MavRequestState.RequestIPAddress);

					if(validSessionId != null)
					{
						AddIdentityMessage(authenticator, validSessionId);
					}
					else
					{
						if(!Regex.Match(authenticationAction, "transparent", RegexOptions.IgnoreCase).Success)
						{
							if(username != null && username.Trim()!= String.Empty)
							{
								AddErrorMessage("Either the username or password supplied was incorrect.");
							}
							else if(action == "login")
							{
								AddErrorMessage("No username was supplied.");
							}
						}
					}
				}
				else
				{
					authenticator.RemoveSession(requestCredentials.AuthenticatedSessionId);
				}				
			}
			else	// The authentication mechanism has not been initialised.
			{
				AddErrorMessage("Authenticator object was not correctly initialised. Check the directory path in the Maverick configuration file.");
			}
			
			// Update the authentication session timestamp.
			// The mechanisms behind establishing and maintaining the session need to be
			// VERY carefully considered if you need to protect something important;
			// setting and retrieving a cookie on the client is not real security! 
			// Suggested approach would be to use this controller over SSL (i.e. https) and only use user agent
			// in memory cookies.
			if(validSessionId != null)
			{
				logger.Debug("SecureXmlController Authenticate obtained a valid session id, " + validSessionId + ".");
				
				// Need to make sure that the cookie timeout value sent to the cookie is in the future. Since
				// timezones will cause problems if working in minutes or hours just set it a day in advance.
				if(sessionState != null)
				{
					// Request the user agent to persist the cookie to persistent storage.
					//MavSessionState.SetSessionValue(AUTHENTICATION_STATE_KEY, validSessionId, DateTime.Now.AddDays(1));
					
					// Request the user agent to store the cookie in memory.
					MavSessionState.SetSessionValue(AUTHENTICATION_STATE_KEY, validSessionId);
				}
				
				return new Credentials(authenticator.GetUsernameForSessionId(validSessionId), null, null);
			}
			else
			{
				logger.Debug("SecureXmlController Authenticate did not obtain a valid session id.");
				
				// Destroy the authentication session, i.e. the expire the authentication session cookie.
				if(sessionState != null)
				{
					//MavSessionState.SetSessionValue(AUTHENTICATION_STATE_KEY, null, DateTime.Now);	

					MavSessionState.SetSessionValue(AUTHENTICATION_STATE_KEY, null);	
				}

				return null;
			}
		}

        public void Logout(AuthenticatorProduct authenticator, RequestState requestState, SessionState sessionState)
        {
            if (authenticator != null)
            {
                string authId = MavRequestState.GetRequestValue(AUTHENTICATION_STATE_KEY);

                if (authId == null || authId.Trim().Length == 0)
                {
                    authId = MavRequestState.GetCookieValue(AUTHENTICATION_STATE_KEY);			// Authentication session state field identifying previous authentication.
                }

                // Create a set of credentials from the reqest information.
                Credentials requestCredentials = new Credentials(null, null, authId);

                authenticator.RemoveSession(requestCredentials.AuthenticatedSessionId);
            }
        }
    }
}
